1 /**
2 Copyright: Copyright (c) 2017, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 
10 This is a handy range to iterate over either all files from the user OR all
11 files in a compilation database.
12 */
13 module compile_db.user_filerange;
14 
15 import logger = std.experimental.logger;
16 import std.algorithm : map, joiner;
17 import std.array : empty, appender, array;
18 import std.range : isInputRange, ElementType, only;
19 import std.typecons : tuple, Nullable;
20 import std.exception : collectException;
21 
22 import my.path : Path, AbsolutePath;
23 
24 import compile_db : CompileCommand, CompileCommandFilter, CompileCommandDB,
25     parseFlag, ParseFlags, Compiler, SystemIncludePath;
26 
27 @safe:
28 
29 struct SimpleRange(T) {
30     private T[] values;
31 
32     this(T[] values) {
33         this.values = values;
34     }
35 
36     T front() {
37         assert(!empty, "Can't get front of an empty range");
38         return values[0];
39     }
40 
41     void popFront() @safe pure nothrow @nogc {
42         assert(!empty, "Can't pop front of an empty range");
43         values = values[1 .. $];
44     }
45 
46     bool empty() @safe pure nothrow const @nogc {
47         import std.array : empty;
48 
49         return values.empty;
50     }
51 
52     size_t length() @safe pure nothrow const @nogc {
53         return values.length;
54     }
55 
56     typeof(this) save() @safe pure nothrow {
57         return typeof(this)(values);
58     }
59 
60     static SimpleRange!T make(T[] values) {
61         return SimpleRange!T(values);
62     }
63 }
64 
65 alias CompileCommandsRange = SimpleRange!CompileCommand;
66 
67 /// Returns: a range over all files in the database.
68 CompileCommandsRange fileRange(CompileCommandDB db) {
69     return CompileCommandsRange(db.payload);
70 }
71 
72 CompileCommandsRange fileRange(Path[] files, Compiler compiler) {
73     import std.file : getcwd;
74 
75     return CompileCommandsRange(files.map!(a => CompileCommand(a, AbsolutePath(a),
76             AbsolutePath(Path(getcwd)), CompileCommand.Command([compiler]),
77             Path.init, AbsolutePath.init)).array);
78 }
79 
80 /// The flags in the CompileCommand are extracted and parsed.
81 struct ParsedCompileCommand {
82     CompileCommand cmd;
83     ParseFlags flags;
84 }
85 
86 alias ParsedCompileCommandRange = SimpleRange!ParsedCompileCommand;
87 
88 /// Returns: a range over all files in the range where the flags have been parsed.
89 auto parse(RangeT)(RangeT r, CompileCommandFilter ccFilter) @safe nothrow 
90         if (is(ElementType!RangeT == CompileCommand)) {
91     return r.map!(a => ParsedCompileCommand(a, parseFlag(a, ccFilter)));
92 }
93 
94 /// Returns: a range wherein the system includes for the compiler has been
95 /// deduced and added to the `flags` data.
96 auto addSystemIncludes(RangeT)(RangeT r) @safe nothrow 
97         if (is(ElementType!RangeT == ParsedCompileCommand)) {
98     static SystemIncludePath[] deduce(CompileCommand cmd, Compiler compiler) @safe nothrow {
99         import compile_db.system_compiler : deduceSystemIncludes;
100 
101         try {
102             return deduceSystemIncludes(cmd, compiler);
103         } catch (Exception e) {
104             logger.info(e.msg).collectException;
105         }
106         return SystemIncludePath[].init;
107     }
108 
109     return r.map!((a) {
110         a.flags.systemIncludes = deduce(a.cmd, a.flags.compiler);
111         return a;
112     });
113 }
114 
115 /// Return: add a compiler to the `flags` data if it is missing.
116 auto addCompiler(RangeT)(RangeT r, Compiler compiler) @safe nothrow 
117         if (is(ElementType!RangeT == ParsedCompileCommand)) {
118     ParsedCompileCommand add(ParsedCompileCommand p, Compiler compiler) @safe nothrow {
119         if (p.flags.compiler.empty) {
120             p.flags.compiler = compiler;
121         }
122         return p;
123     }
124 
125     return r.map!(a => add(a, compiler));
126 }
127 
128 /// Return: replace the compiler in `flags` with `compiler` if `compiler` is
129 /// NOT empty.
130 auto replaceCompiler(RangeT)(RangeT r, Compiler compiler) @safe nothrow 
131         if (is(ElementType!RangeT == ParsedCompileCommand)) {
132     return r.map!((a) {
133         if (!compiler.empty)
134             a.flags.compiler = compiler;
135         return a;
136     });
137 }
138 
139 struct LimitFileRange {
140     /// Files not found in the compile_commands database;
141     string[] missingFiles;
142 
143     ParsedCompileCommand[] commands;
144 
145     bool isMissingFilesEmpty() {
146         return missingFiles.empty;
147     }
148 
149     /// Returns: a range over all files that where found in the database.
150     ParsedCompileCommandRange range() {
151         return ParsedCompileCommandRange.make(commands);
152     }
153 }
154 
155 /// Returns: a struct which has extracted `onlyTheseFiles` into either using
156 /// the matching `ParsedCompileCommand` or missing.
157 LimitFileRange limitFileRange(ParsedCompileCommand[] db, string[] onlyTheseFiles) {
158     import compile_db;
159 
160     auto missing = appender!(string[])();
161     auto app = appender!(ParsedCompileCommand[])();
162     foreach (a; onlyTheseFiles.map!(a => tuple!("file", "result")(a, find(db, a)))) {
163         if (a.result.isNull) {
164             missing.put(a.file);
165         } else {
166             app.put(a.result.get);
167         }
168     }
169 
170     return LimitFileRange(missing.data, app.data);
171 }
172 
173 LimitFileRange limitOrAllRange(T)(ParsedCompileCommand[] db, T[] onlyTheseFiles) {
174     if (onlyTheseFiles.empty)
175         return LimitFileRange(null, db);
176     return limitFileRange(db, onlyTheseFiles.map!(a => cast(string) a).array);
177 }
178 
179 /// Returns: prepend all CompileCommands parsed flags with `flags`.
180 auto prependFlags(RangeT)(RangeT r, string[] flags)
181         if (isInputRange!RangeT && is(ElementType!RangeT == ParsedCompileCommand)) {
182     return r.map!((a) { a.flags.prependCflags(flags); return a; });
183 }
184 
185 /** Find a best matching compile_command in the database against the path
186  * pattern `glob`.
187  *
188  * When searching for the compile command for a file, the compilation db can
189  * return several commands, as the file may have been compiled with different
190  * options in different parts of the project.
191  *
192  * Params:
193  *  glob = glob pattern to find a matching file in the DB against
194  */
195 Nullable!ParsedCompileCommand find(ParsedCompileCommand[] db, string glob) @safe {
196     import compile_db : isMatch;
197 
198     foreach (a; db) {
199         if (isMatch(a.cmd, glob))
200             return typeof(return)(a);
201     }
202     return typeof(return).init;
203 }